package com.sunricher.telinkblemeshlib;

import android.bluetooth.BluetoothGatt;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;

import com.sunricher.telinkblemeshlib.callback.DeviceCallback;
import com.sunricher.telinkblemeshlib.callback.NodeCallback;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;

public class DeviceRepairManager {

    public static DeviceRepairManager getInstance() {
        return Singleton.instance;
    }

    private static final String LOG_TAG = "DeviceRepairManager";

    private Callback callback;
    private MeshNode node;
    private Timer timer;
    private final long scanningInterval = 8000;
    private final long connectingInterval = 16000;
    private final long addressChangingInterval = 8000;

    private HashSet<Integer> existAddresses = new HashSet<>();
    private HashSet<Integer> allAddresses = new HashSet<>();
    private int newAddress = 0;
    private Handler handler = new Handler(Looper.getMainLooper());

    private enum State {
        none, connecting, connected, changing
    }

    private State state = State.none;

    public void setCallback(Callback callback) {
        this.callback = callback;
    }

    private DeviceRepairManager() {

        for (int i = 1; i <= 253; i++) {
            allAddresses.add(i);
        }
    }

    private static final class Singleton {
        private final static DeviceRepairManager instance = new DeviceRepairManager();
    }

    public static abstract class Callback {

        /**
         * 用于修复短地址重复问题。
         */
        public abstract void didFailToConnect();

        /**
         * 连接设备成功，下一步修复。
         */
        public abstract void didConnect();

        /**
         * 在修复过程中，设备断开连接。
         */
        public abstract void didDisconnect();

        /**
         * 修复成功，并返回修复设备的 Mac 及修复后的新短地址。
         */
        public abstract void repairSucceeded(String mac, int newAddress);

    }

    private NodeCallback nodeCallback = new NodeCallback() {

        @Override
        public void didDisconnectNode(MeshManager manager, Boolean isActiveDisConnected, MeshNode node, BluetoothGatt gatt) {
            Log.i(LOG_TAG, "Repair didDisconnectNodeIdentifier");
            if (state != State.connected) {
                return;
            }
            state = State.none;
            if (callback != null) {
                handler.post(() -> {
                    callback.didDisconnect();
                });
            }
        }

        @Override
        public void didFailToConnectNode(MeshManager manager, MeshNode node) {
            Log.i(LOG_TAG, "Repair didFailToConnectNode");
            cancelTimer();
            if (state != State.connecting) {
                return;
            }
            state = State.none;
            if (callback != null) {
                handler.post(() -> {
                    callback.didFailToConnect();
                });
            }
        }

        @Override
        public void didLoginNode(MeshManager manager, MeshNode node) {
            Log.i(LOG_TAG, "Repair didLoginNode");
            cancelTimer();
            if (state != State.connecting) {
                return;
            }
            state = State.connected;
            if (callback != null) {
                handler.post(() -> {
                    callback.didConnect();
                });
            }
            MeshCommand identify = MeshCommand.identify(MeshCommand.Address.connectNode);
            MeshManager.getInstance().send(identify);
            // Connected, then scan devices.
            startScanMeshDevicesTimer();
            manager.scanMeshDevices();
        }

        @Override
        public void didFailToLoginNode(MeshManager manager) {
            Log.i(LOG_TAG, "Repair didFailToLoginNode");
            cancelTimer();
            if (state != State.connecting) {
                return;
            }
            state = State.none;
            if (callback != null) {
                handler.post(() -> {
                    callback.didFailToConnect();
                });
            }
        }

        @Override
        public void didGetDeviceAddress(MeshManager manager, int address) {
            /** Will invoke deviceCallback.didUpdateMeshDevices, don't use this.
            Log.i(LOG_TAG, "Repair didGetDeviceAddress " + address);
            if (state == State.connected) {
                Log.i(LOG_TAG, "state is connected, insert address and restart timer.");
                existAddresses.add(address);
                startScanMeshDevicesTimer();
            } else if (state == State.changing) {
                Log.i(LOG_TAG, "state is changing, if the address is the new address, changing end.");
                if (address == newAddress) {
                    Log.i(LOG_TAG, "new address is the address, change succeeded.");
                    stopRepair();
                    if (callback != null && node != null) {
                        handler.post(() -> {
                            callback.repairSucceeded(node.getMacAddress(), address);
                        });
                    }
                }
            } else {
                Log.i(LOG_TAG, "state is " + state + ", nothing will happen.");
            }
             */
        }

        @Override
        public void didConfirmNewNetwork(MeshManager manager, Boolean isSuccess) {
            super.didConfirmNewNetwork(manager, isSuccess);
        }

        @Override
        public void didGetFirmware(MeshManager manager, String firmware) {
            super.didGetFirmware(manager, firmware);
        }
    };

    private final DeviceCallback deviceCallback = new DeviceCallback() {

        @Override
        public void didUpdateMeshDevices(MeshManager manager, ArrayList<MeshDevice> meshDevices) {

            Log.i(LOG_TAG, "Repair didGetDeviceAddress " + meshDevices);
            if (state == State.connected) {
                Log.i(LOG_TAG, "state is connected, insert address and restart timer.");
                for (MeshDevice device : meshDevices) {
                    existAddresses.add(device.getAddress());
                }
                startScanMeshDevicesTimer();
            } else if (state == State.changing) {
                Log.i(LOG_TAG, "state is changing, if the address is the new address, changing end.");
                for (MeshDevice device : meshDevices) {
                    int address = device.getAddress();
                    if (address == newAddress) {
                        Log.i(LOG_TAG, "new address is the address, change succeeded.");
                        stopRepair();
                        if (callback != null && node != null) {
                            handler.post(() -> {
                                callback.repairSucceeded(node.getMacAddress(), address);
                            });
                        }
                        return;
                    }
                }
            } else {
                Log.i(LOG_TAG, "state is " + state + ", nothing will happen.");
            }
        }
    };

    /**
     * 开始修复，传入需要修复的 MeshNode。
     */
    public void startRepair(MeshNode node) {
        if (node == null) {
            return;
        }
        Log.i(LOG_TAG, "start repair " + node.getDescription());

        state = State.connecting;
        this.node = node;
        existAddresses.clear();
        newAddress = 0;

        MeshManager.getInstance().stopScanNode();
        MeshManager.getInstance().disconnect();
        MeshManager.getInstance().setNodeCallback(nodeCallback);
        MeshManager.getInstance().setDeviceCallback(deviceCallback);
        startConnectTimer();
        MeshManager.getInstance().connect(node);
    }

    /**
     * 立即中止修复。
     */
    public void stopRepair() {

        Log.i(LOG_TAG, "Stop repair");
        state = State.none;
        existAddresses.clear();
        cancelTimer();
        newAddress = 0;

        MeshManager.getInstance().setNodeCallback(null);
        MeshManager.getInstance().setDeviceCallback(null);
        MeshManager.getInstance().stopScanNode();
        MeshManager.getInstance().disconnect();
    }

    private void startConnectTimer() {
        cancelTimer();
        timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                Log.i(LOG_TAG, "connect timer overtime, stop and failed to connect.");
                stopRepair();
                if (callback != null) {
                    callback.didFailToConnect();
                }
            }
        }, connectingInterval);
    }

    private void startScanMeshDevicesTimer() {
        cancelTimer();
        timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                scanMeshDevicesTimerEnd();
            }
        }, scanningInterval);
    }

    // Scan mesh devices finish.
    private void scanMeshDevicesTimerEnd() {
        Log.i(LOG_TAG, "scan mesh devices timer end, will get available address and set a new one.");
        state = State.changing;
        cancelTimer();
        // Get valid address.
        HashSet<Integer> availableAddress = new HashSet<>(allAddresses);
        availableAddress.removeAll(existAddresses);
        if (availableAddress.size() > 0) {
            int address = getRandomAddress(availableAddress);
            Log.i(LOG_TAG, "There is a new address " + address + ", i will set it.");
            this.newAddress = address;
            startChangingTimer();
            MeshCommand cmd = MeshCommand.changeConnectNodeAddress(address);
            MeshManager.getInstance().send(cmd);
        } else {
            Log.i(LOG_TAG, "There are no available addresses, disconnect the node.");
            stopRepair();
            if (callback != null) {
                handler.post(() -> {
                    callback.didDisconnect();
                });
            }
        }
    }

    private int getRandomAddress(HashSet<Integer> addresses) {
        Random random = new Random();
        int randomNumber = random.nextInt(addresses.size());
        Iterator<Integer> iterator = addresses.iterator();
        int currentIndex = 0;
        Integer randomAddress = 123;
        while (iterator.hasNext()) {
            randomAddress = iterator.next();
            if (currentIndex == randomNumber) {
                return randomAddress;
            }
            currentIndex++;
        }
        return randomAddress;
    }

    private void startChangingTimer() {
        cancelTimer();
        timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                Log.i(LOG_TAG, "changing timer timeout, disconnect and change failed.");
                stopRepair();
                if (callback != null) {
                    handler.post(() -> {
                        callback.didDisconnect();
                    });
                }
            }
        }, addressChangingInterval);
    }

    private void cancelTimer() {
        if (timer != null) {
            timer.cancel();
        }
        timer = null;
    }
}
